package jamirr.loader;

import java.io.Closeable;
import java.io.Externalizable;
import java.io.Serializable;
import java.util.*;

abstract class ClassUtil {

    /**
     * Suffix for array class names: {@code "[]"}.
     */
    public static final String ARRAY_SUFFIX = "[]";

    /**
     * Prefix for internal array class names: {@code "["}.
     */
    private static final String INTERNAL_ARRAY_PREFIX = "[";

    /**
     * Prefix for internal non-primitive array class names: {@code "[L"}.
     */
    private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L";

    /**
     * The package separator character: {@code '.'}.
     */
    private static final char PACKAGE_SEPARATOR = '.';

    /**
     * The nested class separator character: {@code '$'}.
     */
    private static final char NESTED_CLASS_SEPARATOR = '$';

    /**
     * Map with common Java language class name as key and corresponding Class as value.
     * Primarily for efficient deserialization of remote invocations.
     */
    private static final Map<String, Class<?>> commonClassCache = new HashMap<>(64);

    /**
     * Map with primitive type name as key and corresponding primitive
     * type as value, for example: {@code "int" -> int.class}.
     */
    private static final Map<String, Class<?>> primitiveTypeNameMap = new HashMap<>(32);

    //proxy frameworks
    private static final List<String> PROXY_CLASS_NAMES = Arrays.asList("net.sf.cglib.proxy.Factory"
            // cglib
            , "org.springframework.cglib.proxy.Factory"

            // javassist
            , "javassist.util.proxy.ProxyObject"
            , "org.apache.ibatis.javassist.util.proxy.ProxyObject");
    private static final String ENHANCER_BY = "$$EnhancerBy";
    private static final String JAVASSIST_BY = "_$$_";

    static {
        primitiveTypeNameMap.put(boolean.class.getName(), boolean.class);
        primitiveTypeNameMap.put(byte.class.getName(), byte.class);
        primitiveTypeNameMap.put(char.class.getName(), char.class);
        primitiveTypeNameMap.put(double.class.getName(), double.class);
        primitiveTypeNameMap.put(float.class.getName(), float.class);
        primitiveTypeNameMap.put(int.class.getName(), int.class);
        primitiveTypeNameMap.put(long.class.getName(), long.class);
        primitiveTypeNameMap.put(short.class.getName(), short.class);
        primitiveTypeNameMap.put(void.class.getName(), void.class);
        primitiveTypeNameMap.put(boolean[].class.getName(), boolean[].class);
        primitiveTypeNameMap.put(byte[].class.getName(), byte[].class);
        primitiveTypeNameMap.put(char[].class.getName(), char[].class);
        primitiveTypeNameMap.put(double[].class.getName(), double[].class);
        primitiveTypeNameMap.put(float[].class.getName(), float[].class);
        primitiveTypeNameMap.put(int[].class.getName(), int[].class);
        primitiveTypeNameMap.put(long[].class.getName(), long[].class);
        primitiveTypeNameMap.put(short[].class.getName(), short[].class);

        commonClassCache.put(Boolean.class.getName(), Boolean.class);
        commonClassCache.put(Byte.class.getName(), Byte.class);
        commonClassCache.put(Character.class.getName(), Character.class);
        commonClassCache.put(Double.class.getName(), Double.class);
        commonClassCache.put(Float.class.getName(), Float.class);
        commonClassCache.put(Integer.class.getName(), Integer.class);
        commonClassCache.put(Long.class.getName(), Long.class);
        commonClassCache.put(Short.class.getName(), Short.class);
        commonClassCache.put(Void.class.getName(), Void.class);
        commonClassCache.put(Boolean[].class.getName(), Boolean[].class);
        commonClassCache.put(Byte[].class.getName(), Byte[].class);
        commonClassCache.put(Character[].class.getName(), Character[].class);
        commonClassCache.put(Double[].class.getName(), Double[].class);
        commonClassCache.put(Float[].class.getName(), Float[].class);
        commonClassCache.put(Integer[].class.getName(), Integer[].class);
        commonClassCache.put(Long[].class.getName(), Long[].class);
        commonClassCache.put(Short[].class.getName(), Short[].class);
        commonClassCache.put(Number.class.getName(), Number.class);
        commonClassCache.put(Number[].class.getName(), Number[].class);
        commonClassCache.put(String.class.getName(), String.class);
        commonClassCache.put(String[].class.getName(), String[].class);
        commonClassCache.put(Class.class.getName(), Class.class);
        commonClassCache.put(Class[].class.getName(), Class[].class);
        commonClassCache.put(Object.class.getName(), Object.class);
        commonClassCache.put(Object[].class.getName(), Object[].class);
        commonClassCache.put(Throwable.class.getName(), Throwable.class);
        commonClassCache.put(Exception.class.getName(), Exception.class);
        commonClassCache.put(RuntimeException.class.getName(), RuntimeException.class);
        commonClassCache.put(Error.class.getName(), Error.class);
        commonClassCache.put(StackTraceElement.class.getName(), StackTraceElement.class);
        commonClassCache.put(StackTraceElement[].class.getName(), StackTraceElement[].class);
        commonClassCache.put(Enum.class.getName(), Enum.class);
        commonClassCache.put(Iterable.class.getName(), Iterable.class);
        commonClassCache.put(Iterator.class.getName(), Iterator.class);
        commonClassCache.put(Enumeration.class.getName(), Enumeration.class);
        commonClassCache.put(Collection.class.getName(), Collection.class);
        commonClassCache.put(List.class.getName(), List.class);
        commonClassCache.put(Set.class.getName(), Set.class);
        commonClassCache.put(Map.class.getName(), Map.class);
        commonClassCache.put(Map.Entry.class.getName(), Map.Entry.class);
        commonClassCache.put(Optional.class.getName(), Optional.class);
        commonClassCache.put(Serializable.class.getName(), Serializable.class);
        commonClassCache.put(Externalizable.class.getName(), Externalizable.class);
        commonClassCache.put(Closeable.class.getName(), Closeable.class);
        commonClassCache.put(AutoCloseable.class.getName(), AutoCloseable.class);
        commonClassCache.put(Cloneable.class.getName(), Cloneable.class);
        commonClassCache.put(Comparable.class.getName(), Comparable.class);
    }

    public static Class<?> resolvePrimitiveClassName(String name) {
        Class<?> result = null;
        // Most class names will be quite long, considering that they
        // SHOULD sit in a package, so a length check is worthwhile.
        if (name != null && name.length() <= 7) {
            // Could be a primitive - likely.
            result = primitiveTypeNameMap.get(name);
        }
        return result;
    }


    public static Class<?> forName(String name) throws LinkageError {
        return forName(name, getDefaultClassLoader());
    }

    public static Class<?> forName(String name, ClassLoader classLoader) throws LinkageError {

        Objects.requireNonNull(name, "Name must not be null");

        Class<?> clazz = resolvePrimitiveClassName(name);
        if (clazz == null) {
            clazz = commonClassCache.get(name);
        }
        if (clazz != null) {
            return clazz;
        }

        // "java.lang.String[]" style arrays
        if (name.endsWith(ARRAY_SUFFIX)) {
            String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length());
            Class<?> elementClass = forName(elementClassName, classLoader);
            return elementClass.arrayType();
        }

        // "[Ljava.lang.String;" style arrays
        if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) {
            String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1);
            Class<?> elementClass = forName(elementName, classLoader);
            return elementClass.arrayType();
        }

        // "[[I" or "[[Ljava.lang.String;" style arrays
        if (name.startsWith(INTERNAL_ARRAY_PREFIX)) {
            String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length());
            Class<?> elementClass = forName(elementName, classLoader);
            return elementClass.arrayType();
        }

        ClassLoader clToUse = classLoader;
        if (clToUse == null) {
            clToUse = getDefaultClassLoader();
        }
        try {
            return Class.forName(name, false, clToUse);
        } catch (ClassNotFoundException ex) {
            int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR);
            int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1);
            if (lastDotIndex != -1 && previousDotIndex != -1 && Character.isUpperCase(name.charAt(previousDotIndex + 1))) {
                String nestedClassName =
                        name.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + name.substring(lastDotIndex + 1);
                try {
                    return Class.forName(nestedClassName, false, clToUse);
                } catch (ClassNotFoundException ex2) {
                    // Swallow - let original exception get through
                }
            }
            throw new RuntimeException(ex);
        }
    }

    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        } catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtil.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                } catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }
}
